require 'logger'
require 'json'
require 'oauth'
require 'net/http'

$tjpLog = Logger.new(STDOUT)
$tjpLog.level = Logger::INFO
$tjpLog.formatter = proc{ |severity, datetime, progname, msg|
    "#{severity} [#{datetime}] in #{progname} -- #{msg}\n"
}

# == 注意
# 這是舊的函式庫，如果你想學習如何使用最新的TJPlurker，建議你應該參考 {此頁}[link:GETSTART.html]
#
# 如果你堅持享用舊版本，你應該在程式中如下引用以支援舊式的寫法：
#  require "tjplurker/legacy"
#  include Legacy
# 而不是
#  require "tjplurker"
#  include TJP
module Legacy
    # == 注意
    # 這是舊的函式庫，如果你想學習如何使用最新的TJPlurker，建議你應該參考 {此頁}[link:GETSTART.html]
    #
    # 如果你堅持享用舊版本，你應該在程式中如下引用以支援舊式的寫法：
    #  require "tjplurker/legacy"
    #  include Legacy
    # 而不是
    #  require "tjplurker"
    #  include TJP
    #
    # 這是TJPlurker的核心類別，大部分的情況下此類別的方法都會回傳一個由Plurk Server產生的JSON物件。
    # 所有的方法會在timeout發生的時候重新呼叫
    #
    # It's core class of TJPlurker.
    # Note that every methods in this class returns a JSON object
    # which was converted from Plurk server's response.
    # All mehtods will retry again when request timeout occurs.
    class TJPlurker
        # 噗浪帳號
        # Plurk user ID.
        attr_reader :user

=begin
[auto_login] 是否在建構時自動登入
             If +auto_login+ sets +true+, it will autologin while constructing.
=end
        def initialize api_key, user, passwd, auto_login=true
            @user, @passwd, @api_key = user, passwd, api_key
            @cookie = nil
            @user_channel = nil
            @id_buffer = nil
            login if auto_login
        end


=begin
:section: Offical API wrapping
對Plurk官方API做包裹

Plurk API wrapping
=end

=begin  
登入
[no_data] 是否將登入成功後的訊息簡化成：
           {"success_text": "ok"}
          否則將回傳使用者的詳細資料
          
          If no_data sets true, the successful login message was simplified as follow:
           {"success_text": "ok"}
          Otherwise, more details of user's profile were retured.
=end
        def login no_data=true
            method = "/API/Users/login"
            attr = {:username=>@user, :password=>@passwd, :api_key=>@api_key}
            attr[:no_data] = '1' if no_data
            api(method, attr)
        end

=begin
發噗

範例：
    require 'tjplurker'
    tjp = TJPlurker.new("API Key", "帳號", "密碼")
    tjp.plurk_add("Hello TJPlurker")
=end
        def plurk_add content, qualifier=':', limited_to=nil, no_comments=nil, lang='tr_ch'
            method = "/API/Timeline/plurkAdd"
            attr = {:api_key=>@api_key, :content=>content, :qualifier=>qualifier,:limited_to=>limited_to, :no_comments=>no_comments, :lang=>lang}
            api(method, attr)
        end

=begin
回噗
[qualifier] 可以是如下的字串：
            
            Can be the following string:
             loves, likes, shares, gives, hates, wants, has, will,
             asks, wishes, was, feels, thinks, says, is, :, freestyle, hopes, needs, wonders
範例：
    require 'tjplurker'
    tjp = TJPlurker.new("API Key", "帳號", "密碼")
    json_obj = tjp.plurk_add("Hello TJPlurker")
    tjp.response_add(json_obj["plurk_id"], "This is a reply.", "says")
=end
        def response_add plurk_id, content, qualifier=':'
            method = "/API/Responses/responseAdd"
            attr = {:api_key=>@api_key, :plurk_id=>plurk_id, :content=>content, :qualifier=>qualifier}
            api(method, attr)
        end

=begin
成為粉絲
[fan_id] 你想追蹤的人的ID
         
         The ID of the user you want to become fan of
=end
        def become_fan fan_id
            method = "/API/FriendsFans/becomeFan"
            attr = {:api_key=>@api_key, :fan_id=>fan_id}
            api(method, attr)
        end

=begin
接受所有的好友邀請

Accept all friendship requests as friends.
Successful return:
 {"success_text": "ok"}
=end
        def add_all_as_friends
            method = "/API/Alerts/addAllAsFriends"
            attr = {:api_key=>@api_key}
            api(method, attr)
        end

=begin
取得使用者的好友名單

Returns user_id's friend list
[limit] The max number of friends to be returned (default 10).
[Successful return] 好友名單陣列

                    Returns a list of JSON objects users, e.g.
                     [{"id": 3, "nick_name": "alvin", ...}, ...]
[warnning] +limit+的最大值被官方設定為100，這個方法沒有很好用，建議你用tjp_get_friends取代
           This maximum of +limit+ is set 100 officially, this method is not that useful.
           We suggest you use get_friend_list instead of get_friends_by_offset.
=end
        def get_friends_by_offset user_id, limit=100, offset='0'
            method = "/API/FriendsFans/getFriendsByOffset"
            attr = {:api_key=>@api_key, :user_id=>user_id, :offset=>offset, :limit=>limit}
            api(method, attr)
        end

=begin
Fetches public information such as a user's public plurks and basic information.
Fetches also if the current user is following the user, are friends with or is a fan.
[Successful return] Returns a JSON object with a lot of information
                    that can be used to construct a user's public profile and timeline.
[Error returns] HTTP 400 BAD REQUEST with {"error_text": "Invalid user_id"} as body
                HTTP 400 BAD REQUEST with {"error_text": "User not found"} as body
=end
        def get_public_profile user_id
            method = "/API/Profile/getPublicProfile"
            attr = {:api_key=>@api_key, :user_id=>user_id}
            api(method, attr)
        end
        
        # Warning:: It has bugs in offecial Plurk API.
        def get_plurks limit=nil, offset=nil
            method = "/API/Polling/getPlurks"
            attr = {:api_key=>@api_key, :limit=>limit, :offset=>offset}
            api(method, attr)
        end

        def get_user_channel
            method = "/API/Realtime/getUserChannel"
            attr = {:api_key=>@api_key}
            api(method, attr)
        end

        def comet_channel
            #Assign new channel if @user_channel is nil
            @user_channel ||= get_user_channel
            begin
                res = Net::HTTP.get_response(URI.parse(@user_channel['comet_server']))
                json = JSON.parse(res.body.sub!(/CometChannel\.scriptCallback\((.*)\);/){|match| $1})
            rescue Timeout::Error => ex
                $tjpLog.warn(self.class){"Request timeout, retry."}
                retry
            rescue Errno::ECONNRESET => ex
                $tjpLog.warn(self.class){"Connection reset, retry."}
                retry
            rescue => ex
                $tjpLog.fatal(self.class){"Unknown error, skip."}
                puts ex.class, ex.message, ex.backtrace
                return
            end
            #Update the offset
            @user_channel['comet_server'].sub!(/[\d]+$/, json["new_offset"].to_s)
            if json['data']
                json['data'].each{|member|
                    notification = NotificationData.new(member)
                    return if notification.id == @id_buffer
                    $tjpLog.info(self.class){"Notification: #{notification.user_id}: \"#{notification.content}\""}
                    @id_buffer = notification.id
                    yield(notification)
                }
            end
            json
        end

=begin
:section: TJPlurker provided
由TJPlurker提供的工具，非官方有的方法

Those methos were all provided by TJPlukrer instead of plurk
=end

=begin
這個方法游TJPlurker提供的非官方功能，改良自get_friends_by_offset，用於取得使用者所有的好友

This method is provided by TJPlurker. Improved from get_friends_by_offset for getting all friends of a user
[max] 取得多少好友，nil則無限制
      
      The maximum number of your request. Set +nil+ for unlimited.
[return] 陣列
         
         An array contains friends
範例：
    require 'tjplurker'
    tjp = TJPlurker.new("API Key", "帳號", "密碼")
    friend_list = tjp.tjp_get_friends("5874158")
    friend_list.each{|friend| puts friend["uid"]}
=end
        def tjp_get_friends user_id, max=nil
            list = []
            offset = 0;
            loop{
                set = get_friends_by_offset(user_id, 100, offset)
                size = set.size
                break if size == 0
                list |= set
                break if max && list.size>max
                offset += size
            }
            return list
        end
        
=begin
用於追蹤大量使用者的河道
[level] 深度，如等於1會追蹤使用者的所有好友，等於2則會追蹤使用者所有的好友以及好友的好友
=end
        def tjp_super_become_fan fan_id, level=1
            return if level<=0
            become_fan(fan_id)
            list = []
            offset = 0;
            loop{
                set = get_friends_by_offset(fan_id, 100, offset)
                size = set.size
                break if size == 0
                tg = ThreadGroup.new
                set.each{|i|
                    tg.add Thread.new{
                        wait_sec = 1
                        until become_fan(i["uid"]).key?("success_text")
                            $tjpLog.info(self.class){"Trying to become #{i["uid"]}'s fan in #{wait_sec} seconds"}
                            sleep(wait_sec)
                            wait_sec *= 2
                        end
                        $tjpLog.info(self.class){"Become #{i["uid"]}'s fan successfully"}
                    }
                }
                tg.list.each{|i| i.join }
                list |= set
                offset += size
            }
            list.each{|i|
                super_become_fan(i["uid"], level-1)
            }
        end
        
        private
        def api method, attr
            #Build POST Request
            req = Net::HTTP::Post.new(method)
            req.set_form_data(attr)
            req["Cookie"] = @cookie
            #Build GET Request
            #path = method + "?" + URI.encode_www_form(attr)
            #req2 = Net::HTTP::Get.new(path)
            #req2["Cookie"] = @cookie
            
            #Build HTTP connection
            http = Net::HTTP.new('www.plurk.com', 80)
            begin
                resp = http.start{
                    $tjpLog.debug(self.class){"Request: #{method}, #{attr}"}
                    http.request(req)
                }
                @cookie ||= resp['set-cookie']
                json = JSON.parse(resp.body)
                $tjpLog.debug(self.class){"Response: #{json}"}
                $tjpLog.error(self.class){json["error_text"]} if json.is_a?(Hash) && json["error_text"]
            rescue Timeout::Error => ex
                $tjpLog.warn(self.class){"Request timeout, retrying."}
                retry
            rescue Errno::ECONNRESET => ex
                $tjpLog.warn(self.class){"Connection reset, retry."}
                retry
            rescue JSON::ParserError => ex
                $tjpLog.error(self.class){"JSON parse error, request failed."}
            rescue => ex
                $tjpLog.fatal(self.class){"Unknown error, skip."}
                puts ex.class, ex.message, ex.backtrace
                return
            end
            return json
        end
        
        # Format the json from the comet server. See TJPlurker#comet_channel
        class NotificationData
            # Can be +new_plurk+ or +new_response+.
            attr_reader :type
            attr_reader :response_count
            attr_reader :plurk_id
            attr_reader :lang
            attr_reader :user_id
            attr_reader :qualifier
            attr_reader :content
            # Notification ID.
            attr_reader :id
            # Time of the listened post.
            attr_reader :posted
            attr_reader :owner_id
            # The original JSON object from server response.
            attr_reader :origin
            
            def initialize member
                @type = member['type']
                @response_count = member['response_count']
                @plurk_id = member['plurk_id']
                
                @lang = @type=="new_plurk" ? member['lang'] : member['response']['lang']
                @user_id = @type=="new_plurk" ? member['user_id'] : member['response']['user_id']
                @qualifier = @type=="new_plurk" ? member['qualifier'] : member['response']['qualifier']
                @content = @type=="new_plurk" ? member['content'] : member['response']['content']
                @id = @type=="new_plurk" ? member['id'] : member['response']['id']
                @posted = @type=="new_plurk" ? member['posted'] : member['response']['posted']
                
                @owner_id = @type=="new_plurk" ? member['owner_id'] : member['plurk']['owner_id']
                
                @origin = member
            end
        end
        
        class Robot
            def initialize *params
                case params.size
                    when 1
                        @tjplurker = params[0]
                    when 3
                        api_key, user, passwd = params
                        @tjplurker = TJPlurker.new(api_key, user, passwd)
                    when 4
                        api_key, user, passwd, auto_login = params
                        @tjplurker = TJPlurker.new(api_key, user, passwd, auto_login)
                end
                @services = []
            end
            
            # Starting a loop and invokes services when a notification was listened.
            def start
                $tjpLog.info(self.class){"Start robot."}
                Thread.new{
                    loop{
                        @tjplurker.add_all_as_friends
                        sleep(60)
                    }
                }
                loop{
                    @tjplurker.comet_channel{|data|
                        @services.each{|service|
                            begin
                                service.serve(@tjplurker, data)
                            rescue => ex
                                $tjpLog.error(self.class){"An error occured in #{service.class}, class: #{ex.class}, message: #{ex.message}, backtrace: #{ex.backtrace}"}
                            end
                        }
                    }
                }
            end
            
            # +service+:: Can be instance TJPlurker::Service or class TJPlurker::Service or it's descendant classes.
            def add_service service
                if service.is_a? Service
                    @services << service
                    $tjpLog.info(self.class){"Add service: #{service.name}"}
                elsif service <= Service
                    tmp = service.new
                    @services << tmp
                    $tjpLog.info(self.class){"Add service: #{tmp.name}"}
                else
                    $tjpLog.warn(self.class){"Unrecognized service, ignored."}
                end
            end
        end
        
        class Service
            attr_reader :name
            def initialize name=self.class.to_s, &proc
                @name = name
                if block_given?
                    @serve_proc = proc
                end
            end
            
            # Invoke when listened a new plurk.
            def decide tjplurker, data
                true
            end
            
            def action tjplurker, data
                if @serve_proc
                    @serve_proc[tjplurker, data]
                else
                    p data
                end
            end
            
            def serve tjplurker, data
                action(tjplurker, data) if decide(tjplurker, data)
            end
        end
        
        class ThreadService < Service
            # +lasting+:: How long in second will a thread lives. If one plurk just be responsed, then it will live another +lasting+ time.
            # +refresh+:: Interval that checks if thread should terminate.
            def initialize name=self.class.to_s, lasting=60, refresh=1, &proc
                super name
                @lasting = lasting
                @refresh = refresh
                @plurk_id_hash = Hash.new
                @mutex = Mutex.new
            end
            
            # Invoke after thread termination.
            # You can use this to tell users this service just terminate.
            def final tjplurker, data
            
            end
            
            def serve tjplurker, data
                if data.type=='new_plurk' && decide(tjplurker, data)
                    @plurk_id_hash[data.plurk_id] = Thread.new(data.plurk_id){|plurk_id|
                        $tjpLog.debug(self.class){"Create thread in plurk ##{plurk_id}"}
                        Thread.current[:last_time] = Time.now.to_i
                        loop{
                            sleep(@refresh)
                            $tjpLog.debug(self.class){"Plurk ##{plurk_id} remain #{Thread.current[:last_time]+@lasting-Time.now.to_i} seconds."}
                            @mutex.lock
                            if Time.now.to_i - Thread.current[:last_time] > @lasting
                                @mutex.unlock
                                break
                            end
                            @mutex.unlock
                        }
                        $tjpLog.debug(self.class){"Terminate thread in plurk ##{plurk_id}"}
                        @mutex.lock
                        @plurk_id_hash.delete(plurk_id)
                        @mutex.unlock
                        final(tjplurker, data)
                    }
                else
                    @mutex.lock
                    if data.type=='new_response' && @plurk_id_hash.has_key?(data.plurk_id)
                        @plurk_id_hash[data.plurk_id][:last_time] = Time.now.to_i
                        action(tjplurker, data)
                    end
                    @mutex.unlock
                end
            end
        end
    end
end